package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"strings"
	"time"

	"gitee.com/Luna-CY/Golang-Project-Template/internal/util/istrings"
	"gitee.com/Luna-CY/Golang-Project-Template/internal/util/los"
	"github.com/spf13/cobra"
)

func NewGenerateDaoCommand() *cobra.Command {
	var table string

	var command = &cobra.Command{
		Use:  "dao",
		Args: cobra.NoArgs,
		Run: func(cmd *cobra.Command, args []string) {
			var pkgName = os.Getenv("GOPACKAGE")

			// dao只能从model生成
			if "model" != pkgName {
				return
			}

			var path = os.Getenv("GOFILE")

			if "" == path {
				cmd.PrintErrf("GOFILE environment variable not set\n")

				os.Exit(1)
			}

			var fset = token.NewFileSet()
			astFile, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
			if nil != err {
				cmd.PrintErrf("error parsing file: %v\n", err)

				return
			}

			ast.Inspect(astFile, func(node ast.Node) bool {
				if tp, ok := node.(*ast.TypeSpec); ok {
					if _, ok := tp.Type.(*ast.StructType); !ok {
						return true
					}

					// do generate interface file
					if err := GenerateDaoDoGenerateInterface(tp.Name.Name); nil != err {
						cmd.PrintErrf("generate interface file failed: %v\n", err)

						return false
					}

					// do generate option file
					if err := GenerateDaoDoGenerateOption(table, tp.Name.Name); nil != err {
						cmd.PrintErrf("generate option file failed: %v\n", err)

						return false
					}

					// do generate implementation file
					if err := GenerateDaoFiles(tp.Name.Name); nil != err {
						cmd.PrintErrf("generate dao files failed: %v\n", err)

						return false
					}
				}

				return true
			})
		},
	}

	command.Flags().StringVar(&table, "table", "", "table name")

	return command
}

func GenerateDaoDoGenerateOption(table string, modelName string) error {
	var template = `package dao

import (
	"fmt"

	"gitee.com/Luna-CY/Golang-Project-Template/internal/errors"
    "gitee.com/Luna-CY/Golang-Project-Template/model"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

// {{.ModelName}}Option {{.ModelName}} 选项
type {{.ModelName}}Option option

// {{.ModelName}}OptionWithLock 选项：加锁查询
func {{.ModelName}}OptionWithLock() {{.ModelName}}Option {
	return func(session *gorm.DB, metadata *Metadata) (*gorm.DB, errors.Error) {
		return session.Clauses(clause.Locking{Strength: SelectForUpdate}), nil
	}
}

// {{.ModelName}}OptionWithOrderDefault 选项：默认排序
func {{.ModelName}}OptionWithOrderDefault() {{.ModelName}}Option {
	return func(session *gorm.DB, metadata *Metadata) (*gorm.DB, errors.Error) {
		return session.Order(fmt.Sprintf("%s.create_time DESC", (&model.{{.ModelName}}{}).TableName())), nil
	}
}

// {{.ModelName}}OptionWithId 选项：根据id查询
func {{.ModelName}}OptionWithId(id ...string) {{.ModelName}}Option {
	return func(session *gorm.DB, metadata *Metadata) (*gorm.DB, errors.Error) {
		if 0 == len(id) {
			return nil, errors.ErrorRecordNotFound("{{.Code}}.33{{.Time}}")
		}

		return session.Where(fmt.Sprintf("%s.id IN ?", (&model.{{.ModelName}}{}).TableName()), id), nil
	}
}
`

	var upperChars = istrings.GetUpperChars(modelName) // 模型名称的全部大写字母
	var minuteAndSecond = time.Now().Format("0405")    // 错误代码定义的分钟与秒数

	var code = fmt.Sprintf("IID_AO.%sOWI_ID", upperChars)
	var content = strings.NewReplacer("{{.ModelName}}", modelName, "{{.TableName}}", table, "{{.Code}}", code, "{{.Time}}", minuteAndSecond).Replace(template)

	path, err := filepath.Abs(filepath.Join("..", "internal", "interface", "dao", fmt.Sprintf("%s_option.go", istrings.CamelCaseToUnderscore(modelName))))
	if nil != err {
		return fmt.Errorf("获取绝对路径失败: %s, err: %s", path, err)
	}

	exists, err := los.CheckPathExists(path)
	if nil != err {
		return fmt.Errorf("检查文件失败: %s, err: %s", path, err)
	}

	if !exists {
		if err := los.WriteToFile(path, content); nil != err {
			return fmt.Errorf("写入文件失败: %s, err: %s", path, err)
		}
	}

	return nil
}

func GenerateDaoDoGenerateInterface(modelName string) error {
	var template = `package dao

import (
	"gitee.com/Luna-CY/Golang-Project-Template/internal/context"
	"gitee.com/Luna-CY/Golang-Project-Template/internal/errors"
    "gitee.com/Luna-CY/Golang-Project-Template/model"
)

// {{.ModelName}} Data access object for {{.ModelName}} definition
type {{.ModelName}} interface {
	Transactional
	
	{{.Methods}}
}

`

	var methods = new(strings.Builder)

	// find by simple condition
	{
		var findByMethodCode = `// Find{{.ModelName}}ByCondition find {{.ModelName}} by simple condition from db
	Find{{.ModelName}}ByCondition(ctx context.Context, page int, size int, options ...{{.ModelName}}Option) (int64, []*model.{{.ModelName}}, errors.Error)`

		methods.WriteString(strings.NewReplacer("{{.ModelName}}", modelName).Replace(findByMethodCode))
	}

	{
		var saveMethodCode = `
	
	// Save{{.ModelName}} save {{.ModelName}} to db
	// if {{.ModelName}} id is 0, it will create a new record, otherwise, it will update the record
	Save{{.ModelName}}(ctx context.Context, {{.LowerModelName}} *model.{{.ModelName}}) errors.Error`

		methods.WriteString(strings.NewReplacer("{{.ModelName}}", modelName, "{{.LowerModelName}}", strings.ToLower(string(modelName[0]))+modelName[1:]).Replace(saveMethodCode))
	}

	{
		// take by condition
		var takeByMethodCode = `

	// Take{{.ModelName}}ByCondition get {{.ModelName}} by condition from db
	// if {{.ModelName}} not found, return error type with errors.ErrorTypeRecordNotFound
	Take{{.ModelName}}ByCondition(ctx context.Context, options ...{{.ModelName}}Option) (*model.{{.ModelName}}, errors.Error)`

		methods.WriteString(strings.NewReplacer("{{.ModelName}}", modelName).Replace(takeByMethodCode))
	}

	{
		// delete by condition
		var deleteByMethodCode = `

	// Delete{{.ModelName}}ByCondition delete {{.ModelName}} by condition from db
	Delete{{.ModelName}}ByCondition(ctx context.Context, options ...{{.ModelName}}Option) errors.Error`

		methods.WriteString(strings.NewReplacer("{{.ModelName}}", modelName).Replace(deleteByMethodCode))
	}

	{
		// batch take by condition
		var batchTakeByMethodCode = `

	// BatchTake{{.ModelName}}ByCondition get {{.ModelName}} by condition from db
	BatchTake{{.ModelName}}ByCondition(ctx context.Context, options ...{{.ModelName}}Option) ([]*model.{{.ModelName}}, errors.Error)`

		methods.WriteString(strings.NewReplacer("{{.ModelName}}", modelName).Replace(batchTakeByMethodCode))
	}

	var content = strings.NewReplacer("{{.ModelName}}", modelName, "{{.Methods}}", methods.String()).Replace(template)

	path, err := filepath.Abs(filepath.Join("..", "internal", "interface", "dao", fmt.Sprintf("%s.go", istrings.CamelCaseToUnderscore(modelName))))
	if nil != err {
		return fmt.Errorf("获取绝对路径失败: %s, err: %s", path, err)
	}

	exists, err := los.CheckPathExists(path)
	if nil != err {
		return fmt.Errorf("检查文件失败: %s, err: %s", path, err)
	}

	if !exists {
		if err := los.WriteToFile(path, content); nil != err {
			return fmt.Errorf("写入文件失败: %s, err: %s", path, err)
		}
	}

	return nil
}

var implementCode = `package {{.UnderscoreModelName}}_dao

import "gitee.com/Luna-CY/Golang-Project-Template/internal/dao"

func New() *{{.ModelName}} {
	return &{{.ModelName}}{
		BaseDao: dao.New(),
	}
}

type {{.ModelName}} struct {
	*dao.BaseDao
}
`

var findByConditionImplementCode = `package {{.UnderscoreModelName}}_dao

import (
	"gitee.com/Luna-CY/Golang-Project-Template/internal/context"
	"gitee.com/Luna-CY/Golang-Project-Template/internal/errors"
	"gitee.com/Luna-CY/Golang-Project-Template/internal/interface/dao"
	"gitee.com/Luna-CY/Golang-Project-Template/internal/logger"
	"gitee.com/Luna-CY/Golang-Project-Template/model"
)

func (cls *{{.ModelName}}) Find{{.ModelName}}ByCondition(ctx context.Context, page int, size int, options ...dao.{{.ModelName}}Option) (int64, []*model.{{.ModelName}}, errors.Error) {
	var session = cls.GetDb(ctx).Model(new(model.{{.ModelName}}))

	var metadata = dao.NewMetadata()
	for _, option := range options {
		var err errors.Error
		if session, err = option(session, metadata); nil != err {
			if err.IsType(errors.ErrorTypeRecordNotFound) {
				return 0, nil, nil
			}

			return 0, nil, err.Relation(errors.ErrorServerInternalError("{{.Code}}.22{{.Time}}", "配置选项失败"))
		}
	}

	var total int64
	if err := session.Count(&total).Error; nil != err {
		logger.SugarLogger(logger.WithRequestId(ctx), logger.WithStack()).Errorf("{{.Code}}.20{{.Time}} count failed, err %v", err)

		return 0, nil, errors.ErrorServerInternalError("{{.Code}}.30{{.Time}}", "统计数据失败")
	}

	if 0 == total || 0 == size || int64((page-1)*size) >= total {
		return total, nil, nil
	}

	var data []*model.{{.ModelName}}
	if err := session.Offset((page - 1) * size).Limit(size).Find(&data).Error; nil != err {
		logger.SugarLogger(logger.WithRequestId(ctx), logger.WithStack()).Errorf("{{.Code}}.31{{.Time}} find failed, err %v", err)

		return 0, nil, errors.ErrorServerInternalError("{{.Code}}.41{{.Time}}", "查询数据失败")
	}

	return total, data, nil
}
`

var saveImplementCode = `package {{.UnderscoreModelName}}_dao

import (
	"gitee.com/Luna-CY/Golang-Project-Template/internal/context"
	"gitee.com/Luna-CY/Golang-Project-Template/internal/errors"
	"gitee.com/Luna-CY/Golang-Project-Template/internal/logger"
	"gitee.com/Luna-CY/Golang-Project-Template/internal/util/pointer"
	"gitee.com/Luna-CY/Golang-Project-Template/model"
	"time"
)

func (cls *{{.ModelName}}) Save{{.ModelName}}(ctx context.Context, {{.LowerModelName}} *model.{{.ModelName}}) errors.Error {
	if nil == {{.LowerModelName}} {
		logger.SugarLogger(logger.WithRequestId(ctx), logger.WithStack()).Errorf("{{.Code}}.15{{.Time}} {{.LowerModelName}} is nil")

		return errors.ErrorServerInternalError("{{.Code}}.17{{.Time}}", "异常的传入参数")
	}

	{{.LowerModelName}}.UpdateTime = pointer.New(time.Now().Unix())
	if nil == {{.LowerModelName}}.CreateTime || 0 == *{{.LowerModelName}}.CreateTime {
		{{.LowerModelName}}.CreateTime = pointer.New(time.Now().Unix())

		if err := cls.GetDb(ctx).Model(new(model.{{.ModelName}})).Create(&{{.LowerModelName}}).Error; nil != err {
			logger.SugarLogger(logger.WithRequestId(ctx), logger.WithStack()).Errorf("{{.Code}}.25{{.Time}} create {{.LowerModelName}} failed, err %v", err)

			return errors.ErrorServerInternalError("{{.Code}}.27{{.Time}}", "创建数据失败")
		}

		return nil
	}

	if err := cls.GetDb(ctx).Model(new(model.{{.ModelName}})).Where("id = ?", {{.LowerModelName}}.Id).Updates(&{{.LowerModelName}}).Error; nil != err {
		logger.SugarLogger(logger.WithRequestId(ctx), logger.WithStack()).Errorf("{{.Code}}.34{{.Time}} save {{.LowerModelName}} failed, err %v", err)

		return errors.ErrorServerInternalError("{{.Code}}.36{{.Time}}", "更新数据失败")
	}

	return nil
}
`

var takeByCode = `package {{.UnderscoreModelName}}_dao

import (
	"gitee.com/Luna-CY/Golang-Project-Template/internal/context"
	"gitee.com/Luna-CY/Golang-Project-Template/internal/errors"
	"gitee.com/Luna-CY/Golang-Project-Template/internal/interface/dao"
	"gitee.com/Luna-CY/Golang-Project-Template/internal/logger"
	"gitee.com/Luna-CY/Golang-Project-Template/model"
	"gorm.io/gorm"
)

func (cls *{{.ModelName}}) Take{{.ModelName}}ByCondition(ctx context.Context, options ...dao.{{.ModelName}}Option) (*model.{{.ModelName}}, errors.Error) {
	if 0 == len(options) {
		logger.SugarLogger(logger.WithRequestId(ctx), logger.WithStack()).Errorf("{{.Code}}.15{{.Time}} options is empty")

		return nil, errors.ErrorServerInternalError("{{.Code}}.17{{.Time}}", "异常的传入参数")
	}

	var session = cls.GetDb(ctx).Model(new(model.{{.ModelName}}))

	var metadata = dao.NewMetadata()
	for _, option := range options {
		var err errors.Error
		if session, err = option(session, metadata); nil != err {
			if err.IsType(errors.ErrorTypeRecordNotFound) {
				return nil, err.Relation(errors.ErrorRecordNotFound("{{.Code}}.26{{.Time}}"))
			}

			return nil, err.Relation(errors.ErrorServerInternalError("{{.Code}}.29{{.Time}}", "配置选项失败"))
		}
	}

	var {{.LowerModelName}} *model.{{.ModelName}}
	if err := session.Take(&{{.LowerModelName}}).Error; nil != err {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, errors.ErrorRecordNotFound("{{.Code}}.36{{.Time}}")
		}

		logger.SugarLogger(logger.WithRequestId(ctx), logger.WithStack()).Errorf("{{.Code}}.39{{.Time}} take {{.LowerModelName}} options failed, err %v", err)

		return nil, errors.ErrorServerInternalError("{{.Code}}.41{{.Time}}", "获取数据失败")
	}

	return {{.LowerModelName}}, nil
}
`

var deleteByCode = `package {{.UnderscoreModelName}}_dao

import (
	"gitee.com/Luna-CY/Golang-Project-Template/internal/context"
	"gitee.com/Luna-CY/Golang-Project-Template/internal/errors"
	"gitee.com/Luna-CY/Golang-Project-Template/internal/interface/dao"
	"gitee.com/Luna-CY/Golang-Project-Template/internal/logger"
	"gitee.com/Luna-CY/Golang-Project-Template/model"
)

func (cls *{{.ModelName}}) Delete{{.ModelName}}ByCondition(ctx context.Context, options ...dao.{{.ModelName}}Option) errors.Error {
	if 0 == len(options) {
		logger.SugarLogger(logger.WithRequestId(ctx), logger.WithStack()).Errorf("{{.Code}}.13{{.Time}} options is empty")

		return errors.ErrorServerInternalError("{{.Code}}.15{{.Time}}", "异常的传入参数")
	}

	var session = cls.GetDb(ctx).Model(new(model.{{.ModelName}}))

	var metadata = dao.NewMetadata()
	for _, option := range options {
		var err errors.Error
		if session, err = option(session, metadata); nil != err {
			if err.IsType(errors.ErrorTypeRecordNotFound) {
				return err.Relation(errors.ErrorRecordNotFound("{{.Code}}.25{{.Time}}"))
			}

			return err.Relation(errors.ErrorServerInternalError("{{.Code}}.28{{.Time}}", "配置选项失败"))
		}
	}

	if err := session.Delete(nil).Error; nil != err {
		logger.SugarLogger(logger.WithRequestId(ctx), logger.WithStack()).Errorf("{{.Code}}.33{{.Time}} delete {{.ModelName}} failed, err %v", err)

		return errors.ErrorServerInternalError("{{.Code}}.35{{.Time}}", "删除数据失败")
	}

	return nil
}
`

var batchTakeByCode = `package {{.UnderscoreModelName}}_dao

import (
	"gitee.com/Luna-CY/Golang-Project-Template/internal/context"
	"gitee.com/Luna-CY/Golang-Project-Template/internal/errors"
	"gitee.com/Luna-CY/Golang-Project-Template/internal/interface/dao"
	"gitee.com/Luna-CY/Golang-Project-Template/internal/logger"
	"gitee.com/Luna-CY/Golang-Project-Template/model"
)

func (cls *{{.ModelName}}) BatchTake{{.ModelName}}ByCondition(ctx context.Context, options ...dao.{{.ModelName}}Option) ([]*model.{{.ModelName}}, errors.Error) {
	if 0 == len(options) {
		logger.SugarLogger(logger.WithRequestId(ctx), logger.WithStack()).Errorf("{{.Code}}.13{{.Time}} options is empty")

		return nil, errors.ErrorServerInternalError("{{.Code}}.15{{.Time}}", "异常的传入参数")
	}

	var session = cls.GetDb(ctx).Model(new(model.{{.ModelName}}))

	var metadata = dao.NewMetadata()
	for _, option := range options {
		var err errors.Error
		if session, err = option(session, metadata); nil != err {
			if err.IsType(errors.ErrorTypeRecordNotFound) {
				return nil, nil
			}

			return nil, err.Relation(errors.ErrorServerInternalError("{{.Code}}.28{{.Time}}", "配置选项失败"))
		}
	}

	var data []*model.{{.ModelName}}
	if err := session.Find(&data).Error; nil != err {
		logger.SugarLogger(logger.WithRequestId(ctx), logger.WithStack()).Errorf("{{.Code}}.34{{.Time}} batch take by condition failed, err %v", err)

		return nil, errors.ErrorServerInternalError("{{.Code}}.36{{.Time}}", "批量获取数据失败")
	}

	return data, nil
}
`

func GenerateDaoFiles(modelName string) error {
	var underscoreModelName = istrings.CamelCaseToUnderscore(modelName) // 模型名称的蛇形命名
	var upperChars = istrings.GetUpperChars(modelName)                  // 模型名称的全部大写字母
	var last2Chars = strings.ToUpper(modelName[len(modelName)-2:])      // 错误代码定义的最后两位
	var minuteAndSecond = time.Now().Format("0405")                     // 错误代码定义的分钟与秒数

	root, err := filepath.Abs(filepath.Join("..", "internal", "dao", underscoreModelName+"_dao"))
	if nil != err {
		return fmt.Errorf("获取绝对路径失败: %s, err: %s", root, err)
	}

	if err := os.MkdirAll(root, 0755); nil != err {
		return fmt.Errorf("创建文件夹失败: %s, err: %s", root, err)
	}

	var kvs = []string{"{{.ModelName}}", modelName, "{{.LowerModelName}}", strings.ToLower(string(modelName[0])) + modelName[1:], "{{.UnderscoreModelName}}", underscoreModelName, "{{.Time}}", minuteAndSecond}

	{
		// generate impl.go
		var path = filepath.Join(root, "impl.go")

		if err := los.WriteToFile(path, strings.NewReplacer(kvs...).Replace(implementCode)); nil != err {
			return fmt.Errorf("写入文件失败: %s, err: %s", path, err)
		}
	}

	{
		var code = fmt.Sprintf("%s.%s.%s", "ID"+upperChars+"_"+last2Chars, upperChars+"_"+last2Chars, "F"+upperChars+"BC_ON")
		var kvs = append(kvs, "{{.Code}}", code)

		// generate impl_{{UnderscoreModelName}}_find_{{UnderscoreModelName}}_by_condition.go
		var path = filepath.Join(root, fmt.Sprintf("impl_%s_find_%s_by_condition.go", underscoreModelName, underscoreModelName))

		exists, err := los.CheckPathExists(path)
		if nil != err {
			return fmt.Errorf("检查文件失败: %s, err: %s", path, err)
		}

		if !exists {
			if err := los.WriteToFile(path, strings.NewReplacer(kvs...).Replace(findByConditionImplementCode)); nil != err {
				return fmt.Errorf("写入文件失败: %s, err: %s", path, err)
			}
		}
	}

	{
		var code = fmt.Sprintf("%s.%s.%s", "ID"+upperChars+"_"+last2Chars, upperChars+"_"+last2Chars, "S"+upperChars+"_"+last2Chars)
		var kvs = append(kvs, "{{.Code}}", code)

		// generate impl_{{UnderscoreModelName}}_save_{{UnderscoreModelName}}.go
		var path = filepath.Join(root, fmt.Sprintf("impl_%s_save_%s.go", underscoreModelName, underscoreModelName))

		exists, err := los.CheckPathExists(path)
		if nil != err {
			return fmt.Errorf("检查文件失败: %s, err: %s", path, err)
		}

		if !exists {
			if err := los.WriteToFile(path, strings.NewReplacer(kvs...).Replace(saveImplementCode)); nil != err {
				return fmt.Errorf("写入文件失败: %s, err: %s", path, err)
			}
		}
	}

	{
		var code = fmt.Sprintf("%s.%s.%s", "ID"+upperChars+"_"+last2Chars, upperChars+"_"+last2Chars, "T"+upperChars+"BC_ON")
		var kvs = append(kvs, "{{.Code}}", code)

		// generate impl_{{UnderscoreModelName}}_take_{{UnderscoreModelName}}_by_condition.go
		var path = filepath.Join(root, fmt.Sprintf("impl_%s_take_%s_by_condition.go", underscoreModelName, underscoreModelName))

		exists, err := los.CheckPathExists(path)
		if nil != err {
			return fmt.Errorf("检查文件失败: %s, err: %s", path, err)
		}

		if !exists {
			if err := los.WriteToFile(path, strings.NewReplacer(kvs...).Replace(takeByCode)); nil != err {
				return fmt.Errorf("写入文件失败: %s, err: %s", path, err)
			}
		}
	}

	{
		var code = fmt.Sprintf("%s.%s.%s", "ID"+upperChars+"_"+last2Chars, upperChars+"_"+last2Chars, "D"+upperChars+"BC_ON")
		var kvs = append(kvs, "{{.Code}}", code)

		// generate impl_{{UnderscoreModelName}}_delete_{{UnderscoreModelName}}_by_condition.go
		var path = filepath.Join(root, fmt.Sprintf("impl_%s_delete_%s_by_condition.go", underscoreModelName, underscoreModelName))

		exists, err := los.CheckPathExists(path)
		if nil != err {
			return fmt.Errorf("检查文件失败: %s, err: %s", path, err)
		}

		if !exists {
			if err := los.WriteToFile(path, strings.NewReplacer(kvs...).Replace(deleteByCode)); nil != err {
				return fmt.Errorf("写入文件失败: %s, err: %s", path, err)
			}
		}
	}

	{
		var code = fmt.Sprintf("%s.%s.%s", "ID"+upperChars+"_"+last2Chars, upperChars+"_"+last2Chars, "BT"+upperChars+"BC_ON")
		var kvs = append(kvs, "{{.Code}}", code)

		// generate impl_{{UnderscoreModelName}}_batch_take{{UnderscoreModelName}}_by_condition.go
		var path = filepath.Join(root, fmt.Sprintf("impl_%s_batch_take_%s_by_condition.go", underscoreModelName, underscoreModelName))

		exists, err := los.CheckPathExists(path)
		if nil != err {
			return fmt.Errorf("检查文件失败: %s, err: %s", path, err)
		}

		if !exists {
			if err := los.WriteToFile(path, strings.NewReplacer(kvs...).Replace(batchTakeByCode)); nil != err {
				return fmt.Errorf("写入文件失败: %s, err: %s", path, err)
			}
		}
	}

	return nil
}
